################################################################################
# Imports
################################################################################

import Education
import ycp
import re
import csv
import crypt
import random
import string
import EducationGroup

from EducationGUIHelper import *

ycp.import_module("SCR")
ycp.import_module("FileUtils")

from ycp import SCR, Path, FileUtils

################################################################################
# Constants
################################################################################

# Minimum username length
MIN_USERNAME_LENGTH = 2

# Maximum username length
MAX_USERNAME_LENGTH = 32

# Minimum password length
MIN_PASSWORD_LENGTH = 0

# Maximum password length
MAX_PASSWORD_LENGTH = 72

# Regex for user and group names.  Must begin with a letter or underscore,
# and consist only of letters, digits, underscores, periods, or hypens.
USERNAME_REGEX = '^[a-zA-Z_][a-zA-Z0-9_.-]*$'

# Regex for a valid password
PASSWORD_REGEX = '^[-0-9a-zA-Z!@#$%^&*() ,;:._+/|?{}=[\'"`~<>]|]*$'

DISPLAY_PASSWORD = "******"

USER_ADD_CMD='/usr/sbin/useradd'
USER_MOD_CMD='/usr/sbin/usermod'
USER_DEL_CMD='/usr/sbin/userdel'

# Minimum number of columns in the users CSV file
MIN_IMPORT_USER_COLUMNS = 4

# Maximum number of columns in the users CSV file
MAX_IMPORT_USER_COLUMNS = 6

################################################################################
# Classes
################################################################################

class User:

  def __init__(self, username=None):

    self.is_new = (username == None)
    self.previous_username = username
    self.on_disk_username = username
    self.username = (username or '')
    self.deleted = False
    
    self.full_name = ""

    if (self.is_new):
      self.password = None
    else:
      self.password = DISPLAY_PASSWORD
      
    self.group = None    
    self.auto_login = False
    self.enabled = True
    self.modified = False
    self.username_changed = False
    self.password_changed = False
    self.enabled_changed = False
    self.undo_state = None
    self.renamed = False

  def set_username(self, username, loading=False):

    self.modified |= (username != self.username) and not loading
    self.username_changed = (username != self.on_disk_username) and not loading
    self.username = username    

  def set_full_name(self, full_name, loading=False):
    self.modified |= (full_name != self.full_name) and not loading
    self.full_name = full_name
    
  def set_password(self, new_password):
    self.password = new_password
    self.modified = True     # We don't know the current password anyway, so if it changes, the user is modified
    self.password_changed = True

  def set_group(self, group_name, loading=False):

    print "setting group " + group_name
    if Education.get_config('groups').has_key(group_name):
      new_group = Education.get_config('groups')[group_name]
      new_group.add_member(self, loading)
      self.group = new_group
      print self.group.name

  def set_auto_login(self, auto_login, loading=False):
    self.modified |= (auto_login != self.auto_login) and not loading

    if (auto_login):
      for username in Education.get_config('users'):
        Education.get_config('users')[username].auto_login = False
      
    self.auto_login = auto_login
  
  def set_enabled(self, enabled, loading=False):
    self.modified |= (enabled != self.enabled) and not loading
    self.enabled_changed |= (enabled != self.enabled) and not loading
    self.enabled = enabled

  def password_changed(self):
    return self.password_changed

  def get_auto_login_string(self):
    return "%s" % (self.auto_login and "Yes" or "No")

  def get_enabled_string(self):
    return "%s" % (self.enabled and "Yes" or "No")

  def validate_user(self, existing_users, education_groups):

    # Stores error messages returned by invalid data
    errors = []

    # Ensure the username is valid and that it doesn't already exist
    if (len(self.username) == 0):
      errors.append("No username specified.")
    elif ((len(self.username) < MIN_USERNAME_LENGTH) or (len(self.username) > MAX_USERNAME_LENGTH)):
      errors.append("Invalid username length.  Must be between %d and %d." % (MIN_USERNAME_LENGTH, MAX_USERNAME_LENGTH))
    elif (re.match(USERNAME_REGEX, self.username) == None):
      errors.append("Invalid username characters.")
    elif ((self.username in existing_users) and (self.username != self.previous_username)):
      errors.append("The specified username already exists.")

    # Validate the password
    if (self.password_changed) and ((len(self.password) < MIN_PASSWORD_LENGTH) or (len(self.password) > MAX_PASSWORD_LENGTH)):
      errors.append("Invalid password length.  Must be between %d and %d." % (MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH))
    elif (self.password_changed) and (re.match(PASSWORD_REGEX, self.password) == None):
      errors.append("Invalid password characters.")

    # Ensure the group specified for the user is a valid education group
    if (not self.group) or (not self.group.name in education_groups):
      errors.append("The specified education group does not exist.")

    # Ensure the auto_login setting is valid
    try:
      # Must convert to int first since the import process expects a 0 or a 1
      self.auto_login = bool(int(self.auto_login))
    except:
      errors.append("Invalid auto login value.  Must be 0 or 1.")

    # Ensure the enabledsetting is valid
    try:
      # Must convert to int first since the import process expects a 0 or a 1
      self.enabled = bool(int(self.enabled))
    except:
      errors.append("Invalid enabled value.  Must be 0 or 1.")

    return (len(errors) == 0, errors)

  def save(self):

    if (self.is_new) or (self.username == self.previous_username):
      Education.get_config('users')[self.username] = self
    else:
      # Rename the user
      Education.get_config('users').pop(self.previous_username)
      Education.get_config('users')[self.username] = self

    self.previous_username = self.username

  def delete(self):
    self.deleted = True

  def save_state(self):

    self.undo_state = {}

    self.undo_state['is_new'] = self.is_new
    self.undo_state['previous_username'] = self.previous_username
    self.undo_state['on_disk_username'] = self.on_disk_username
    self.undo_state['username'] = self.username

    self.undo_state['full_name'] = self.full_name
    self.undo_state['password'] = self.password
    self.undo_state['group'] = self.group
    self.undo_state['auto_login'] = self.auto_login
    self.undo_state['enabled'] = self.enabled
    self.undo_state['modified'] = self.modified
    self.undo_state['deleted'] = self.deleted
    self.undo_state['username_changed'] = self.username_changed
    self.undo_state['password_changed'] = self.password_changed
    self.undo_state['enabled_changed'] = self.enabled_changed

  def restore_state(self):

    if (self.undo_state == None):
      return

    self.is_new = self.undo_state['is_new']
    self.previous_username = self.undo_state['previous_username']
    self.on_disk_username = self.undo_state['on_disk_username']
    self.username = self.undo_state['username']

    self.full_name = self.undo_state['full_name']
    self.password = self.undo_state['password']
    self.group = self.undo_state['group']
    self.auto_login = self.undo_state['auto_login']
    self.enabled = self.undo_state['enabled']
    self.modified = self.undo_state['modified']
    self.deleted = self.undo_state['deleted']
    self.username_changed = self.undo_state['username_changed']
    self.password_changed = self.undo_state['password_changed']
    self.enabled_changed = self.undo_state['enabled_changed']

    self.undo_state = None

  def write_to_disk(self):

    path = ".eduuser.value." + self.username

    SCR.Write(Path(path + ".FULL_NAME"), self.full_name)    
    SCR.Write(Path(path + ".GROUP"), self.group.name)    
    SCR.Write(Path(path + ".AUTO_LOGIN"), self.auto_login and "yes" or "no")
    SCR.Write(Path(path + ".ENABLED"), self.enabled and "yes" or "no")

    if (self.is_new) or ((self.username not in get_system_users() and not self.renamed)):

        command = USER_ADD_CMD + " "                                        # Add a new user
        command += "-m "                                                    # Create a home directory for the user

        if (self.full_name) and len(self.full_name.strip()) > 0:
          command += "-c " + self.full_name + " "                           # Add user's full name

        command += "-p " + crypt.crypt(self.password, self.username) + " "  # Set user's password
        command += self.username
        print command

        SCR.Execute(Path(".target.bash"), command)

        if (self.enabled_changed):
          command = USER_MOD_CMD + (self.enabled and " -U " or " -L ") + self.username
          SCR.Execute(Path(".target.bash"), command)
     
    elif (self.modified):

      command = USER_MOD_CMD + " "                                          # Modify the user

      if (self.full_name) and len(self.full_name.strip()) > 0:
        command += "-c " + self.full_name + " "                             # Set user's full name        

      # If the user was renamed
      if (self.renamed):
        command += "-l " + self.username + " "                              # Rename it to the new username

        # Determine its new home directory
        new_home_dir = get_default_home() + self.username

        # If the new home directory does not exist
        if not FileUtils.Exists(new_home_dir):
          command += "-d " + new_home_dir + " "                             # Specify the user's new home
          command += "-m "                                                  # Move the old directory          

      if (self.password_changed):
        command += "-p " + crypt.crypt(self.password, self.username) + " "  # Set user's password

      if (self.enabled_changed):
        command += (self.enabled and " -U " or " -L ")                      # Enable/disable the user

      command += self.on_disk_username

      print command
      SCR.Execute(Path(".target.bash"), command)

################################################################################
# Module functions
################################################################################

def delete_users():

  for user in Education.get_config('deleted_users'):

    if (user.deleted) and not (user.is_new):
      SCR.Execute(Path(".target.bash"), USER_DEL_CMD + " -r " + user.on_disk_username)
      SCR.Execute(Path(".target.remove"),"/etc/sysconfig/education/users/" + user.on_disk_username)      

# If a user's username is changed, we will first rename the user to a temporary
# username before renaming the user to the new desired username (done in User.save())
# This is done for safety in the event that user1 was renamed to user2,
# and user2 was renamed to user1.  For efficiency, we could calculate a dependency
# graph, but this should suffice and guarantees safety.
def rename_users():

  default_home = get_default_home()
  system_users = get_system_users()

  for username in Education.get_config('users'):

    user = load_user(username)

    # If we have a user whose username was changed, who is not new, and who actually exists on disk...
    # Not being a new user and existing on disk are not necessarily the same thing.  After all, someone
    # could have deleted the user with userdel but left their username in the education configuration files
    if (user.username_changed) and not (user.is_new) and (user.on_disk_username in system_users):
  
      temp_username = get_temp_username()

      command = USER_MOD_CMD + " "                           # Modify the user
      command += "-l " + temp_username + " "                 # Rename the user to the temporary username
      command += "-d " + default_home + temp_username + " "  # Specify its new temporary home directory
      command += "-m "                                       # Rename its old home directory
      command += user.on_disk_username                       # Old username

      print "Renaming user " + user.on_disk_username + " to " + temp_username
      system_users.remove(user.on_disk_username)
      SCR.Execute(Path(".target.bash"), command)
      SCR.Execute(Path(".target.remove"),"/etc/sysconfig/education/users/" + user.on_disk_username)
      user.on_disk_username = temp_username
      user.renamed = True
      system_users.append(temp_username)

# Returns a unique username that does not exist on disk, and for which no home directory currently 
# exists in $HOME/{username}
def get_temp_username():

  default_home = get_default_home()
  system_users = get_system_users()
  temp_username = ''.join([random.choice(string.ascii_letters + string.digits) for x in xrange(8)])

  while ((temp_username in system_users) or
        ((not re.match(USERNAME_REGEX, temp_username)) or FileUtils.Exists(default_home + temp_username))):
    temp_username = ''.join([random.choice(string.ascii_letters + string.digits) for x in xrange(8)])

  return temp_username

def get_default_home():

  default_home = SCR.Read(Path(".etc.default.useradd.HOME"))

  if not default_home:
    default_home = "/"
  elif default_home[0] != '/':
    default_home = "/" + default_home

  if default_home[-1] != '/':
    default_home += "/"

  return default_home


def commit_user_changes():

  # Remove all users that were deleted
  delete_users()

  # Temporarily rename "renamed" users to avoid conflicts
  rename_users()

  print "After delete/rename users"
  for username in Education.get_config('users'):
    user = load_user(username)
    user.write_to_disk()

# Returns a list of all education users
def get_education_users():
  users = Education.get_config('users').keys()
  users.sort()

  for username in Education.get_config('users').keys():
    user = load_user(username)
    if (user.deleted):
      users.remove(username)

  return users

# Returns a list of all group names (system and education groups)
def get_existing_users():

  saved_education_users = []
  current_education_users = []

  for username in Education.get_config('users'):
    user = load_user(username)
    saved_education_users.append(user.on_disk_username)
    current_education_users.append(username)

  for user in Education.get_config('deleted_users'):
    saved_education_users.append(user.on_disk_username)
    
  # Forget about the on disk education usernames since they might have been renamed
  existing_users = list(set(get_system_users()) - set(saved_education_users))

  # Add in their current names
  existing_users.extend(current_education_users)

  # Sort the user list and return it
  existing_users.sort()

  print existing_users
  return existing_users

# Retrieves a list of all usernames in /etc/passwd
def get_system_users():

  users = []
  userdata = SCR.Read(Path(".etc.passwd"))

  for user in userdata:
    users.append(user['username'])

  users.sort()
  return users

def get_users_table_data():

  users = []

  for username in Education.get_config('users'):

    user = Education.get_config('users')[username]

    if (user.deleted):
      continue
    
    username_cell = icon_cell("yast-education-user.png", username)    
    users.append(item(username, [username_cell, user.full_name, user.group and user.group.name or "None", user.get_auto_login_string(), user.get_enabled_string()]))

  return users

# Deletes the specified user from the configuration
def delete_user(username):
  user = load_user(username)

  if (user.group):
    user.group.remove_member(user)
    
  user.deleted = True
  Education.get_config('users').pop(username)
  Education.get_config('deleted_users').append(user)

def load_user(username):
  return Education.get_config('users')[username]

def ensure_all_users_have_groups():

  for username in get_education_users():

    user = load_user(username)
    
    if not user.deleted and user.group == None:
      return False

  return True

##
# Reads user data from a CSV file.  The file must be in the format:
#
# USERNAME, FULL_NAME, EDUCATION_GROUP_NAME, PASSWORD, AUTOMATIC_LOGIN, ENABLED
#
# where the last two options (AUTOMATIC_LOGIN, ENABLED) are optional.  If specified,
# they must be 1 (true) or 0 (false).
#
# Parameters:
#
#   filename: The CSV file from which to import
#
# Returns:
#
#   imported_users: A map of maps of all successfully imported users indexed by
#                   user name.  The map is of the form:
#
#                         'jeff' :  {
#                                     'username': 'jeff',
#                                     'full_name': 'Jeff Shantz',
#                                     'password': 'test',
#                                     'group': 'Adults',
#                                     'auto_login': False,
#                                     'enabled': True
#                                   }
#
#   errors: A list of error messages generated while parsing the CSV file
##
def import_users(filename):

  existing_users = get_existing_users()
  education_groups = EducationGroup.get_education_groups()

  # Stores the data for all users imported
  imported_users = []

  # Stores error messages returned by invalid data
  errors = []

  # Open the file and initialize the CSV reader
  content = SCR.Read(Path('.target.string'), filename).splitlines()
  reader = csv.reader(content)
  line = 0

  # Iterate through each row in the file
  for row in reader:

    line += 1

    # Check that we have a valid number of columns
    if ((len(row) < MIN_IMPORT_USER_COLUMNS) or (len(row) > MAX_IMPORT_USER_COLUMNS)):

      errors.append("Line %d: Invalid number of columns -- must be between %d and %d." % (line, MIN_IMPORT_USER_COLUMNS, MAX_IMPORT_USER_COLUMNS))
      continue

    else:

      # Reverse the columns since pop() operates from the end of the array
      row.reverse()

      # Stores the new group
      user = User()
      
      # Get all raw group data
      user.set_username(row.pop().strip())
      user.set_full_name(row.pop().strip())
      user.set_password(row.pop().strip())
      group = EducationGroup.load_group(row.pop().strip())

      if group:
        user.set_group(group.name)

      # If we have more data left in the row, grab the auto_login setting
      if (len(row) > 0):
        user.set_auto_login(bool(int(row.pop().strip())))

      # If we still have more data left in the row, grab the enabled setting
      if (len(row) > 0):
        user.set_enabled(bool(int(row.pop())))

      (is_valid, user_errors) = user.validate_user(existing_users, education_groups)

      if (is_valid):
        # All is well, so add the user to the imported users map
        imported_users.append(user)
        existing_users.append(user.username)
      else:
        for error in user_errors:
          errors.append("%d:%s" % (line, error))

  # Return both the imported users map and the error list
  return (imported_users, errors)